Lab 06 - Klasy - rozszerzenie
Lab 06 - Klasy i wzorce - usystematyzowanie wiadomości
Informacje wstępne
Use case klasy Histogram
Jako przykład wprowadzający stworzymy klasę histogram, której celem jest reprezentacja częstości wystąpień konkretnych elementów zbioru. Histogram reprezentuję częstość wystąpień elementów w pewnym zbiorze
Klasa histogramu powinna umożliwiać gromadzenie danych i ich integrację, wizualizację oraz konwersję. Dla przykładowego zbioru danych z rysunku, wprowadzenie pełnego zbioru przedstawionego na rysunku powinno skutkować stworzeniem następującej reprezentacji wewnętrznej histogramu:
przedział | wartość |
---|---|
1 | 4 |
2 | 2 |
7 | 1 |
8 | 1 |
9 | 1 |
W dalszym przykładzie zastosowania klasy zostanie stworzona klasa Histogram
umożliwiająca reprezentację ocen. Dane wejściowe zawierają wyniki pewnego egzaminu wyrażone w punktach w skali 0-10pkt. W ogólności, histogram może reprezentować częstość występowania danych w przedziałach obejmujących kilka wartości elementów (wtedy liczba przedziałów będzie mniejsza niż liczba unikalnych wartości zbioru danych). W przykładzie jednak zakładamy, że każda unikalna wartość jest osobnym przedziałem histogramu.
Zakładamy przy tym następujący use case:
Definiowanie obiektów (konstruktory)
//Konstruktor domyślny
Histogram hist; std::vector<int>({10,15,6,9,10,12})); // definiuje obiekt wywołując konstruktor inicjujący go listą wyników w punktach Histogram hist_2(
Metody wprowadzania i modyfikacji danych do histogramu
20); //dodaje ocenę 20pkt
hist.emplace(
std::vector<int>({10, 15, 6, 9, 10, 12}));
hist.emplace(
10 << 12 << 20 << 21; // kolejne elementy zawierają punktację poszczególnych studentów
hist <<
// pobiera dane od użytkownika (pobierając najpierw liczbę wyników, które chce wprowadzić)
cin >> hist;
R"(../wyniki.csv)", ',', 4); //wczytuje plik csv, argumentami są nazwa pliku, separator kolumn oraz index kolumny w której znajdują się dane dla histogramu
hist.from_csv(
//usuwa wszystkie dane z histogramu // jeśli dane nie zostaną usunięte kolejne wywołania operatora zapisu do strumienia lub hist_2.clear();
Operacje na histogramie
using Bin = int; // alias typu wartości dla przedziału histogramu
using Frequency = int; // alias typu wartości dla częstości wystąpień
std::cout << hist; // zapis histogramu do strumienia tekstowego
std::ofstream file("histogram.txt");
// zapis histogramu do pliku (identycznie jak dla wyświetlenia go na konsoli)
file << hist;
int freq = hist[10.5]; // zwraca częstość dla przedziału do którego należy wartość 10.5
std::pair<Bin, Bin> range = hist.range(); // zwraca początkowy i końcowy przedział.
std::pair<Bin, Frequency> max = hist.max(); // zwraca najczęściej występujący przedział oraz jego częstość
std::vector<Bin> bins = hist.unique_bins(); // zwraca listę unikalnych, niepustych przedziałów
std::vector<std::pair<Bin, Frequency>> items = hist.unique_items(); // zwraca listę niepustych przedziałów oraz częstość wystąpień
//konwersja na inne typy
using BinsVector = std::vector<std::pair<Bin, Frequency>>;
static_cast<BinsVector>(hist); // operator rzutowania działa tak samo jak Histogram::unique_items
BinsVector items_vect = // zewnętrzna funkcja wyświetlająca wektor elementów histogramu print(items_vect);
🛠🔥 Zadanie 🛠🔥
- Przeanalizuj powyższy przykład użycia oraz umieszczone komentarze wyjaśniające. W oparciu o swoją dotychczasową wiedzę (nie analizując dalszego ciągu wprowadzenia) wyodrębnij konstruktory, metody oraz operatory niezbędne do jego uruchomienia. Zastanów się jakie atrybuty (pola) musi mieć klasa
Histogram
, tak by możliwa była reprezentacja i uaktualnianie histogramu. Podpowiedź: Na zajęciach Lab04 realizowałeś program wyświetlający częstość wystąpień słów z pliku tekstowego, zastanów się jak wykorzystaną tam reprezentację zastosować w klasieHistogram
. Jeśli masz problem ze stworzeniem odpowiedniej reprezentacji metod i konstruktora, zapoznaj się z: - Deklarację klasy umieść w pliku nagłówkowym
histogram.h
i dołącz go do swojego pliku źródłowego zawierającego funkcję główną oraz zamieszczony powyżej przykład użycia. Spróbuj skompilować program, zobacz dla których linii przykładu użycia kompilator zwraca błąd (np.Identifier ... not found
,no member named ...
,no variable conversion from...
). Jeśli proces budowania wyświetla tylko błędy linkera (undefined reference to ...
), oznacza to ze udało Ci się stworzyć deklarację klasy zgodną z przykładem użycia.
Pamiętaj, żeby plik nagłówkowy zawierał dyrektywy preprocesora pełniące rolę strażnika załączenia:
#ifndef HISTOGRAM_H
#define HISTOGRAM_H
class Histogram
{
//deklaracje pól i metod
};#endif
ewentualnie:
#pragma once
class Histogram
{
//deklaracje pól i metod
};
- Popraw deklarację klasy
Histogram
stosując następujące zalecenia:- wszystkie pola klasy deklaruj jako prywatne,
- wszystkie obiekty przekazywane jako argument metody/funkcji, powinny być przekazywane przez referencję
Type ¶m
(jeśli funkcja/metoda je modyfikuje) lub referencję do stałejconst Type& param
(jeśli są wykorzystywane do odczytu) - zabieg ten pozwoli na skrócenie czasu wykonania metody, ponieważ argument nie będzie kopiowany, - wszystkie metody, które nie modyfikują zawartości obiektu (należące do grupy przykładu użycia
Operacje na histogramie
) zadeklaruj jako metody stałe (umieszczając słowo kluczoweconst
na końcu deklaracji metody). Stosowanie metod stałych umożliwia ich wywołanie dla obiektu, który jest stałą (np. został przekazany jako referencja do stałej). Ponadto, dzięki temu w interfejsie klasy możemy jawnie wskazać, które metody nie modyfikują wewnętrznego stanu obiektu. np. dla metodyrange
:
// deklaracja wewnątrz deklaracji klasy Histogram (plik histogram.h):
std::pair<Bin, Bin> range() const;
//-------------------------
// definicja (plik histogram.cpp)
std::pair<Bin, Bin> Histogram::range() const{
// ciało funkcji
return {min,max};
}
Po stworzeniu prawidłowej deklaracji spróbuj stworzyć definicje poszczególnych metod klasy
Histogram
umieszczając je w odrębnym plikuhistogram.cpp
. W metodach tj.Histogram::max
,Histogram::unique_items
spróbuj wykorzystać algorytmy STLstd::max_element
,std::copy
a jeśli trzeba, wyrażenia lambda.Spróbuj zaimplmenetować uniwersalną metodę wczytującą
Histogram::from_csv
z argumentami umożliwiającymi podanie nazwy pliku, separatora elementów oraz kolumny w której znajdują się wartości wejściowe dla histogramu. Wczytaj załączony do zadania plik, który zawiera wyniki egzaminu w formie tabelarycznej, oddzielonej przecinkami, a wynik punktowy danej osoby jest w kolumnie 5. Uruchom przykład przekazując pobrany plik z danymi, możesz go również przetestować na danych z innym separatorem i układem kolumn.
Deklaracja klasy Histogram
Poniższej zaprezentowano pełną deklarację klasy histogram, jednak zachęcamy Ciebie żebyś z niej skorzystał tylko w celu weryfikacji lub porównania napisanego przez Ciebie kodu.
class Histogram
{std::map<int, int> bins_;
using BinsVector = std::vector<std::pair<int, int>>;
public:
const std::vector<int> &data = std::vector<int>());
Histogram(void emplace(int v);
void emplace(const std::vector<int> &data);
void clear();
bool from_csv(const std::string &filename, char delim = ',', int column_idx = 4);
std::pair<int, int> max() const;
std::pair<int, int> range() const;
std::vector<int> unique_bins() const;
const;
BinsVector unique_items()
operator<<(int v);
Histogram& static Histogram generate(int min, int max, int count);// tworzy histogram zawierający wartości losowe, gdzie kolejne argumenty oznaczają wartość minimalną, maksymalną, liczbę elementów
int operator[](int v) const; // zwraca częstość dla binu w indeksie
operator BinsVector(); // operator konwersji na typ `BinsVector` (`std::vector<std::pair<int, int>>`)
friend std::istream &operator>>(std::istream &str, Histogram &hist);
friend std::ostream &operator<<(std::ostream &str, const Histogram &hist);
};
Metody statyczne
Załóżmy, że przykład użycia wzbogacony zostanie o możliwość inicjacji wartościami losowymi.
10, 10, 100); // tworzy histogram zawierający wartości losowe, gdzie kolejne argumenty oznaczają wartość minimalną, maksymalną, liczbę elementów
Histogram hist_3 = Histogram::generate(-100, [](){ return -10 + rand() % 21; }); // tworzy histogram zawierający wartości losowe, liczbę elementów do losowania oraz wskaźnik do funkcji losującej pojedynczy element Histogram hist_3 = Histogram::generate(
Żeby napisać deklaracje i definicje metod Histogram::generate
z przykładu użycia trzeba wyjaśnić 2 zagadnienia
Metody statyczne
Podane wyżej metody Histogram::generate
są wywoływane bez konieczności użycia obiektu (instancji klasy). Metody takie nazywa się metodami statycznymi.
- Deklaracja metody statycznej jest identyczna jak zwykłej metody tylko przed deklaracją umieszczone jest słowo kluczowe
static
. - Definicja metody statycznej podlega pewnym ograniczeniom, ponieważ nie może ona odwoływać się do pól oraz metod, które nie są statyczne.
deklaracja metody Histogram::generate
pasującej do pierwszego przykładu użycia może mieć postać:
static Histogram generate(int min, int max, int counter);
Natomiast definicja może mieć postać:
int count, int (*func_ptr)())
Histogram Histogram::generate(
{
Histogram h;for (int i = 0; i < counter; i++)
h.emplace(func_ptr());return h;
}
Wskaźniki do funkcji
W drugim przykładzie użycia Histogram::generate
, jako argument przekazywana jest funkcja generująca pojedynczą próbkę. Dotychczas przekazywałeś wielokrotnie funkcję jako argument wywołania (np. w algorytmach STL), teraz dowiesz się w jaki sposób zdefiniować funkcję, której argumentem jest wyrażenie lambda lub inna funkcja.
W ogólności, dla funkcji zadeklarowanej jako:
typ_zwracany nazwa_funkcji(typ_arg1, typ_arg2);
Funkcja ta, może być reprezentowana i wywoływana za pomocą swojej nazwy lub wskaźnika definiowanego następująco:
// deklaracja zmiennej typu wskaźnik do funkcji
typ_zwracany (*nazwa_wskaznika)(typ_arg1, typ_arg2)); // przypisanie funkcji 'nazwa_funkcji' do wskaźnika
nazwa_wskaznika = nazwa_funkcji;
//wywołanie funkcji 'nazwa_funkcji' przez wskaźnik do funkcji - wywołanie jest równoważne zapisowi nazwa_funkcji(arg1, arg2) nazwa_wskaznka(arg1, arg2);
podczas deklarowania typów funkcyjnych, użyteczne może być wykorzystanie aliasów using
:
using typ_funkcji = typ_zwracany (*)(typ_arg1, typ_arg2); // zdefiniowanie typu funkcji
nullptr; // deklaracji wskaźnika na funkcję (chwilowo nie wskazuje na nic)
typ_funkcji nazwa_wskaznika = // - deklaracja wygląda jak deklaracja zwykłej zmiennej
// przypisanie funkcji `nazwa_funkcji` do wskaźnika
nazwa_wskaznika = nazwa_funkcji;
// analogicznie do poporzedniego przykładu typ_funkcji nazwa_wskaznika2 = nazwa_funkcji;
Przypisanie funkcji do wskaźnika jest możliwe wtedy i tylko wtedy gdy ściśle zgodny jest typ funkcji oraz typy i liczba argumentów
Załóżmy następujące funkcje:
float sum (float a, float b){
return a + b;
}
float mul (float a, float b){
return a * b;
}
Wtedy możliwe jest następujące wykorzystanie wskaźnika do funkcji:
float (*operacja)(float a, float b); //deklaracja zmiennej typu wskaźnik do funkcji
// lub równoważnie
using operacja_typ = float (*)(float a, float b); // definicja typu
// deklaracja zmiennej typu wskaźnik do funkcji
operacja_typ operacja;
// przypisuje funkcję sum
operacja = sum; std::cout << operacja(3, 3); // wyswietli 6 - dodawanie
// przypisuje funkcję mul
operacja = mul; std::cout << operacja(3, 3); // wyswietli 9 - mnożenie
float a, float b){ return a - b;}; // przypisuje wyrażenie lambda
operacja = [](std::cout << operacja(3, 3); // wyswietli 0 - odejmowanie za pomocą wyrażenia lambda
🛠🔥 Zadanie 🛠🔥
- Wskaźnik do funkcji może również być argumentem wywołania innej funkcji, wtedy jeden z argumentów funkcji jest typu wskaźnika do funkcji określającego typ, oraz argumenty funkcji. Na podstawie przedstawionych powyżej informacji stwórz deklarację i definicję metody statycznej
Histogram::generate
umożliwiającej wywołanie w postaci:
100, [](){ return -10 + rand() % 21; }); // tworzy histogram zawierający wartości losowe, liczbę elementów do losowania oraz wskaźnik do funkcji losującej pojedynczy element Histogram hist_3 = Histogram::generate(
Szablony
Funkcje szablonowe
Szablony (ang. templates) są kolejnym mechanizmem wprowadzonym w języku C++ pozwalającym zmniejszyć częstość pojawianie się duplikatów w kodzie.
Szablonem w C++ może zostać dowolna funkcja, metoda, struktura czy klasa poprzez dodanie słowa kluczowego template
oraz listy parametrów szablonowych <typename T>
przed deklaracją/definicją odpowiedniego elementu.
Załóżmy, że mamy dany szablon funkcji
template <typename T>
std::vector<std::vector<T>> createMatrix(int m, int n) {
\\ ... }
Zadaniem powyższej funkcji ma być utworzenie i zwrócenie macierzy o wymiarach m
na n
. Gdybyśmy mieli do czynienia z konkretnym typem np. int
to macierz mogłaby zostać utworzona w następujący sposób:
std::vector<std::vector<int>> A(m);
for (unsigned int i = 0; i < m; i++) {
std::vector<int>(n);
A[i] = }
Dla typu szablonowego zapis jest analogiczny:
std::vector<std::vector<T>> A(m);
for (unsigned int i = 0; i < m; i++) {
std::vector<T>(n);
A[i] = }
W momencie procesu specjalizacji szablonu T
zostanie zastąpione odpowiednim typem wskazanym przez użytkownika/programistę.
Kompletna funkcja będzie miała następującą postać:
template <typename T>
std::vector<std::vector<T>> createMatrix(int m, int n) {
std::vector<std::vector<T>> A(m);
for (unsigned int i = 0; i < m; i++) {
std::vector<T>(n);
A[i] =
}return A;
}
Ponieważ wśród jawnych argumentów funkcji nie pojawia się żaden z typem T
w związku z tym trzeba wprost określić ten typ w czasie wywołania, tzn.:
std::vector<std::vector<int>> A1 = createMatrix<int>(5,5);
std::vector<std::vector<float>> A2 = createMatrix<float>(5,5);
std::vector<std::vector<complex>> A3 = createMatrix<complex>(5,5);
W momencie kompilacji na podstawie zostaną wygenerowane następujące funkcje:
std::vector<std::vector<int>> createMatrix(int m, int n) {
std::vector<std::vector<int>> A(m);
for (unsigned int i = 0; i < m; i++) {
std::vector<int>(n);
A[i] =
}return A;
}std::vector<std::vector<float>> createMatrix(int m, int n) {
std::vector<std::vector<float>> A(m);
for (unsigned int i = 0; i < m; i++) {
std::vector<float>(n);
A[i] =
}return A;
}std::vector<std::vector<complex>> createMatrix(int m, int n) {
std::vector<std::vector<complex>> A(m);
for (unsigned int i = 0; i < m; i++) {
std::vector<complex>(n);
A[i] =
}return A;
}
Oczywiście trzeba tutaj zaznaczyć, że gdybyśmy ręcznie powyższe funkcje zaimplementowali w ten sposób kompilacja by się nie powiodła – mamy trzy funkcje o tej samej nazwie i liście argumentów.
Kolejny przypadek wykorzystania szablonów obejmuje występowanie typu szablonowego na liście argumentów funkcji, np.:
template <typename T>
std::vector<std::vector<T>> copy(const std::vector<std::vector<T>>& matrix) {
unsigned int m = matrix.size();
unsigned int n = matrix[0].size();
std::vector<std::vector<T>> matrixCopy = createMatrix(m, n);
for (unsigned int i = 0; i < m; i++) {
for (unsigned int j = 0; j < n; j++) {
matrixCopy[i][j] = matrix[i][j];
}return matrixCopy;
}
Przykład programu wykorzystujący tę funkcję może wyglądać następująco:
auto A = createMatrix<int>(5, 6);
auto B = copy(A);
Jak można zauważyć w wywołaniu funkcji copy
nie potrzeba już jawnie określać typu szablonu.
Szablony struktur i klas
Zdefiniowanie szablonów struktury lub klasy polega na umieszczeniu przed jej deklaracją słowa kluczowego template
oraz listy parametrów szablonowych <typename T>
. Załóżmy, że chcemy stworzyć szablon klasy Histogram
tak by możliwe było zliczanie nie tylko liczb całkowitych (jak w poprzenim przykłądzie, lecz również elementów dowolnych typów. Dla uprzednio zdefiniowanej klasy Histogram
wykorzystanie szablonów wygląda następująco: (funkcje i klasy szablonowe wraz z implementacją umieszcza się w 99% przypadków w plikach nagłówkowych)
// fragment klasy Histogram z poprzednich przykładów
template <typename T>
class Histogram
{std::map<T, int> bins_;
using BinsVector = std::vector<std::pair<T, int>>;
public:
const std::vector<T> &data = std::vector<T>()) {
Histogram(// to implement
}void emplace(const T& v);{
// to implement
}void emplace(const std::vector<T> &data) {
// to implement
}
void clear() {
// to implement
}bool from_csv(const std::string &filename, char delim = ',', int column_idx = 4) {
// to implement
}std::pair<T, int> max() const {
// to implement
}std::pair<T, T> range() const {
// to implement
}std::vector<T> unique_bins() const {
// to implement
} };
Analizują przykład można zauważyć, że typ szablonowy może zostać zastosowany zarówno w typach pól struktury/klasy (bins_
) jak i w jej metodach czy konstruktorach. Należy pamiętać, że wszystkie funkcje i struktury szablonowe muszą być zaimplementowane w plikach nagłówkowych!
Wykorzystanie powyższego szablonu umieszcza się zazwyczaj w plikach *.cpp, w których dołączany jest nagłówek z zdefiniowanym szablonem. Np.:
#include "histogram.h"
int main()
{int> histogram_int;
Histogram<float> histogram_float;
Histogram<std::string> histogram_of_names;
Histogram<
5);
histogram_int.emplace(12.3f);
histogram_float.emplace(
"John");
histogram_of_names.emplace("John");
histogram_of_names.emplace("John");
histogram_of_names.emplace("Maria");
histogram_of_names.emplace("Maria");
histogram_of_names.emplace(
std::pair<std::string, int> mostFrequent = histogram_of_names.max();
// lub
auto mostFrequent2 = histogram_of_names.max();
}
Uwaga: Ponieważ kompilator w momencie generowania implementacji funkcji, struktury musi wiedzieć wszystko danym szablonie (musi znać również ciało funkcji szablonowej), przez co wszystkie szablony powinny być umieszczone w pliku nagłówkowym modułu.
Zadanie domowe 🏠🔥
Zadanie 1
Stwórz pełną deklarację szablonu klasy Histogram
i uruchom ją dla kilku przykładów, gdzie wejściem są słowa (np. plik) i liczby (np. plik). Spróbuj tak zparametryzować metodę Histogram::from_csv
, żeby umożliwiała wczytanie obu plików. Zaimplementuj również szablonową funkcję print
Zadanie 2
Z wykorzystaniem szablonów zaimplementuj obsługę liczb zespolonych, gdzie precyzja reprezentacji części rzeczywistej i urojonej jest określona przez użytkownika/programistę biblioteki. Wykorzystując szablonową wersję implementacji liczb zespolonych napisz program, który je wykorzystuje. Użyj typów bazowych: int, float, double
.
Zadanie 3
Z wykorzystaniem szablonów zaimplementuj operacje dodawania, odejmowania i mnożenia skalarnego dwóch wektorów o długości n
(tablice jednowymiarowe) oraz wyświetlania na standardowym wyjściu. Następnie przetestuj działanie dla par wektorów o następujących typach: int, float, complex
. Wektory możesz wypełnić wartościami losowymi. Wykorzystaj zaimplementowana wcześniej bibliotekę liczb zespolonych.
Zadanie 4
Napisz klasę Matrix
reprezentującą macierz i umożliwiającą wykorzystanie dla następującego przykładu użycia:
// tworzy macierze 3x3
double> M(3, 3);
Matrix<double> C({{1, 0, 0},
Matrix<0, 1, 0},
{0, 0, 1}});
{double> D;
Matrix<
std::cin >> D; // pobiera dane od użytkownika (zarówno jej wymiar jak i wartości poszczególnych elementów)
int> X = Matrix::eye(3,3); // metoda statyczna, zwraca macierz jednostkową
Matrix<std::cout << X << std::endl;
// inicjalizacja zmienną losową
static std::default_random_engine e{};
std::uniform_int_distribution<int> distriubution{0, 100};
int> Y = Matrix::fill(3, 3, [&distribution](){ return distribution(e); }); // metoda statyczna, zwraca macierz o wymiarze 3x3, wypełnioną wartościami generowanymi przez funkcję będącą trzecim argumentem
Matrix<
double> B = 5 * M * D * 5 + 1; // operacje arytmetyczne na macierzach - zdefiniuj wszystkie niezbędne operatory
Matrix<std::cout << B << std::endl;
Uwagi: - operacja Macierz + skalar
powinna dodawać do każdego z elementów macierzy wartość skalara, - zwróć uwagę, że skalar * Macierz
, Macierz * skalar
to dwa różne operatory. - operacje dodawania/mnożenia macierzy powinny weryfikować prawidłowość rozmiaru macierzy, będących argumentami, w przypadku niemożliwości przeprowadzenia operacji (wskutek niedopasowania wymiaru) metoda powinna zwracać standardowy wyjątek std::out_of_range
(patrz przykład z wykładu).
- Tworząc klasę zwróć uwagę na:
- spróbuj najpierw napisać deklarację klasy pasującą do przykładu użycia, a dopiero gdy kompilacja się powiedzie, definicję poszczególnych metod
- wszystkie obiekty przekazywane jako argument metody/funkcji, powinny być przekazywane przez referencję (jeśli funkcja/metoda je modyfikuje) lub referencję do stałej (jeśli są wykorzystywane do odczytu) - zabieg ten pozwoli skrócić czas wykonania metody, ponieważ argument nie będzie kopiowany
- wszystkie metody, które nie modyfikują zawartości obiektu zadeklaruj jako metody stałe (umieszczając słowo kluczowe
const
na końcu deklaracji metody). - wszystkie pola klasy deklaruj jako prywatne,
- pola klasy, jeśli to możliwe inicjuj na liście inicjalizacyjnej a nie wewnątrz ciała konstruktora.
Autorzy: Piotr Kaczmarek, Przemysław Walkowiak